﻿//
// © Algobox Ltd - Benn Eichhorn
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Algobox.Toolbox.Collections;
using Algobox.Objects.Finance.Assets.Stock;
using System.ComponentModel;
using Algobox.Feeds.Finance.MarketData;
using Algobox.Toolbox.Collections.TimeSeries;
using System.Threading;

namespace Algobox.Structure.Finance.Strategies.Terrapin
{
    public class TerrapinStock : Strategy, INotifyPropertyChanged, IMarketUpdateLast, ITerrapinUnitCallback
    {
        #region Constructors

        public TerrapinStock(TerrapinManagement manager,
            Algobox.Feeds.Finance.Trading.ITradingFeedAdaptor tradingFeed,
            StockSecurity security,
            Guid databaseId,
            string account,
            int unitValue,
            int numEntryBars,
            int numExitBars,
            int periodFastEMA,
            int periodSlowSMA)
            : base(security)
        {
            _isEnabled = false;
            _manager = manager;
            _tradingFeed = tradingFeed;
            DatabaseId = databaseId;
            Account = account;
            _security = security;
            _unitValue = unitValue;
            _numEntryBars = numEntryBars;
            _numExitBars = numExitBars;
            _periodFastEMA = periodFastEMA;
            _periodSlowSMA = periodSlowSMA;
            _id = _numEntryBars + ":" + _numExitBars + ":" + _periodFastEMA + ":" + _periodSlowSMA;

            Phase = Phases.Initialised;
            _averageFilter = AverageFilter.Flat;
            _barIndex = -1;

            if (security.Currency == Objects.Finance.Currencies.Currency.GBX)
                _divisor = 100;
            else
                _divisor = 1;

            Log.Info("{0}|{1}|TerrapinStock Creating Terrapin strategy with a unit value of {2} with Account({3}) DatabaseId({4})", _security, _id, unitValue, account, databaseId);
        }

        #endregion
        //////////
        #region Member Variables

        const int BAR_SIZE = 5; // number of minutes for each bar

        private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();

        public event PropertyChangedEventHandler PropertyChanged;

        private TerrapinManagement _manager;
        private Algobox.Feeds.Finance.Trading.ITradingFeedAdaptor _tradingFeed;
        private StockSecurity _security;
        string _id;
        private Phases _phase;
        private OHLCMinuteBars _bars5min;
        private OHLCMinuteBars _bars1min;
        private object _lock = new object();

        private bool _isEnabled;
        private string _account;
        private int _unitValue;
        private int _numEntryBars;
        private int _numExitBars;
        private int _periodFastEMA;
        private int _periodSlowSMA;
        private int _divisor;
        private bool _isExiting;

        private double _last;
        private double _high;
        private double _low;
        private double _fastEMA;
        private double _slowSMA;
        private double _mid;
        private int _barIndex;
        private int _barsInitCount;

        private AverageFilter _averageFilter;

        private TerrapinUnit _triggeredUnit;

        #endregion
        //////////
        #region Properties

        public Guid DatabaseId { get; private set; }
        public string Name { get { return Security.ToString() + "-" + _id; } }

        public bool IsEnabled
        {
            get { return _isEnabled; }
            set
            {
                lock (_lock)
                    _isEnabled = value;

                if (value && _phase == Phases.FillFailure)
                {
                    Log.Info("{0}|{1}|IsEnabled User has re-enabled after a fill failure", _security, _id);
                    _phase = Phases.BuildingData;
                }

                Log.Info("{0}|{1}|IsEnabled {2} ", _security, _id, value ? "enabled" : "disabled");
                NotifyPropertyChanged("IsEnabled");
            }
        }

        public string Account
        {
            get { return _account; }
            set
            {
                if (Log.IsInfoEnabled)
                    Log.Info("{0}|{1}|Account changing from {2} to {3}", _security, _id, _account, value);

                lock (_lock)
                    _account = value;

                NotifyPropertyChanged("Account");
            }
        }
        
        public int UnitValue { get { return _unitValue; } set { Log.Info("{0}|{1}|UnitValue changing from {2} to {3}", _security, _id, _unitValue, value); _unitValue = value; NotifyPropertyChanged("UnitValue"); } }
        public Phases Phase { get { return _phase; } private set { Log.Info("{0}|{1}|Phase changing from {2} to {3}", _security, _id, _phase, value); _phase = value; NotifyPropertyChanged("Phase"); } }
        public string Sector { get { return _security.Sector; } }

        public int EntryBars { get { return _numEntryBars; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|EntryBars changing from {2} to {3}", _security, _id, _numEntryBars, value); _numEntryBars = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodFastEMA + ":" + _periodSlowSMA; NotifyPropertyChanged("EntryBars"); NotifyPropertyChanged("Security"); } }
        public int ExitBars { get { return _numExitBars; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|ExitBars changing from {2} to {3}", _security, _id, _numExitBars, value); _numExitBars = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodFastEMA + ":" + _periodSlowSMA; NotifyPropertyChanged("ExitBars"); NotifyPropertyChanged("Security"); } }
        public int FastEMA { get { return _periodFastEMA; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|FastEMA changing from {2} to {3}", _security, _id, _periodFastEMA, value); _periodFastEMA = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodFastEMA + ":" + _periodSlowSMA; NotifyPropertyChanged("FastEMA"); NotifyPropertyChanged("Security"); } }
        public int SlowSMA { get { return _periodSlowSMA; } set { if (Log.IsInfoEnabled) Log.Info("{0}|{1}|SlowSMA changing from {2} to {3}", _security, _id, _periodSlowSMA, value); _periodSlowSMA = value; _id = _numEntryBars + ":" + _numExitBars + ":" + _periodFastEMA + ":" + _periodSlowSMA; NotifyPropertyChanged("SlowSMA"); NotifyPropertyChanged("Security"); } }

        public double Last { get { return _last; } private set { _last = value; NotifyPropertyChanged("Last"); } }
        public double High { get { return _high; } }
        public double Low { get { return _low; } }
        public double MAFast { get { return _fastEMA; } }
        public double MASlow { get { return _slowSMA; } }
        public AverageFilter Filter { get { return _averageFilter; } private set { _averageFilter = value; NotifyPropertyChanged("Filter"); } }

        #endregion
        //////////
        #region Functions

        public override string ToString()
        {
            return Security.ToString() + "-" + _id;
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public void OnPriceLast(IStock stock, Objects.Finance.Prices.PriceVolume last)
        {
            if (Monitor.TryEnter(_lock, 10))
            {
                try
                {
                    //if(last.TimeStamp.TimeOfDay >= new TimeSpan(9,40,0))
                    //{
                    //    Log.Info("{0}|{1}|OnPriceLast Initialising price bars to {2} minutes between {3} and {4}", _security, _id, _bars.BarSize, _bars.StartTime, _bars.EndTime);
                    //}
                    int barIndex5min, barIndex1min;
                    if (_bars5min == null)
                    {
                        // itinialise bars to 8am fixed range
                        //_bars = new OHLCMinuteBars(DateTime.Now.Date.AddHours(8), DateTime.Now.Date.AddHours(16.5), TimeSpan.FromMinutes(BAR_SIZE));

                        // initialiase bars to begin when the first datapoint arrives
                        _bars5min = new OHLCMinuteBars(last.TimeStamp, last.TimeStamp.Date.AddHours(16.5), TimeSpan.FromMinutes(BAR_SIZE));
                        _bars1min = new OHLCMinuteBars(last.TimeStamp, last.TimeStamp.Date.AddHours(16.5), TimeSpan.FromMinutes(1));

                        if (Log.IsInfoEnabled)
                        {
                            Log.Info("{0}|{1}|OnPriceLast Initialising price bars to {2} minutes between {3} and {4}", _security, _id, _bars5min.BarSize, _bars5min.StartTime, _bars5min.EndTime);
                        }

                        _bars5min.TryAdd(last.TimeStamp, last.Price, last.Volume, out barIndex5min);
                        _bars1min.TryAdd(last.TimeStamp, last.Price, last.Volume, out barIndex1min);

                        // set the barIndex member to the starting point of the first datapoint
                        // this is necessary only if _bars is initialised with a fixed time point such as 8am
                        // otherwise if initialised to an arbitrary time point such as the first price timestamp then
                        // barIndex will always start at 0
                        _barIndex = barIndex5min;
                    }
                    else
                    {
                        _bars5min.TryAdd(last.TimeStamp, last.Price, last.Volume, out barIndex5min);
                        _bars1min.TryAdd(last.TimeStamp, last.Price, last.Volume, out barIndex1min);
                    }

                    switch (_phase)
                    {
                        case Phases.Initialised:
                            Phase = Phases.BuildingData;
                            // do something with the data
                            break;

                        case Phases.BuildingData:
                            // check if we have enough data built
                            // all bars need to be full of data before starting so must be > than num entry bars required
                            if (barIndex5min != _barIndex)
                            {
                                int numBars = barIndex5min - _barIndex;
                                if (numBars + _barsInitCount > _numEntryBars)
                                {
                                    _barsInitCount = _numEntryBars + 1;
                                }
                                else
                                {
                                    _barsInitCount += numBars;
                                }
                                _barIndex = barIndex5min;

                                // save previous moving average value
                                _slowSMA = _fastEMA;
                            }
                            if (_barsInitCount >= _numEntryBars)
                            {
                                Phase = Phases.Analysing;
                                GetFilter(barIndex1min);
                                GetChannel(barIndex5min);
                                _mid = _bars1min.Mid;
                            }
                            break;

                        case Phases.Analysing:
                            // get current channel
                            if (barIndex5min != _barIndex)
                            {
                                GetChannel(barIndex5min);
                                _barIndex = barIndex5min;

                                // save previous moving average value
                                _slowSMA = _fastEMA;
                                GetFilter(barIndex1min);
                            }
                            // get new filter if required                            
                            else if (last.Price != _last)
                            {
                                GetFilter(barIndex1min);
                            }

                            double high, low;
                            high = _bars5min.High;
                            low = _bars5min.Low;

                            //// BETA VERSION TO USE LAST
                            if (high > _high && low < _low)
                            {
                                if (Log.IsInfoEnabled)
                                    Log.Info("{0}|{1}|OnPriceLast Filtered price as current bar is both higher than high and lower than low, waiting till next bar BarHigh({2}) BarLow({3}) High({4}) Low({5})", _security, _id, high, low, _high, _low);

                                _barsInitCount -= 1;
                                Phase = Phases.BuildingData;
                            }
                            else
                            {
                                CheckBuyEntry(last);
                                CheckSellEntry(last);
                            }
                            //// BETA VERSION TO USE LAST

                            break;

                        case Phases.Triggered:
                            System.Diagnostics.Debug.Assert(_triggeredUnit != null, "Phase is triggered, but unit is null");

                            if (last.TimeStamp.TimeOfDay >= TerrapinManagement.TIME_EXIT_ALL)
                            {
                                if (!_isExiting)
                                {
                                    _isExiting = true;

                                    if (Log.IsInfoEnabled)
                                        Log.Info("{0}|{1}|OnPriceLast Triggered order has reached it's exit time", _security, _id);

                                    _triggeredUnit.ExitUnit(last.TimeStamp);                                    
                                }
                            }
                            else
                            {
                                _triggeredUnit.OnBestPrice(last.Price, last.TimeStamp);
                            }

                            // update the exit price if we are a new bar
                            if (_barIndex != barIndex5min)
                            {
                                if (_triggeredUnit != null)
                                {
                                    if (!_triggeredUnit.IsBuy && _bars5min.TryGetPreviousHigh(_barIndex - 1, _numExitBars, out _high))
                                    {
                                        _triggeredUnit.ExitTarget = _high;
                                        NotifyPropertyChanged("High");
                                    }
                                    else if (_triggeredUnit.IsBuy && _bars5min.TryGetPreviousLow(_barIndex - 1, _numExitBars, out _low))
                                    {
                                        _triggeredUnit.ExitTarget = _low;
                                        NotifyPropertyChanged("Low");
                                    }
                                }
                                GetChannel(_barIndex + 1);
                                _barIndex = barIndex5min;
                            }
                            break;
                    } // switch
                }
                finally
                {
                    Last = last.Price;
                    Monitor.Exit(_lock);
                }
            }
            else if (Log.IsTraceEnabled)
            {
                Log.Trace("{0}|{1}|OnPriceLast couldn't enter lock after 10 milliseconds", _security, _id);
            }
        }

        private void CheckBuyEntry(Objects.Finance.Prices.PriceVolume price)
        {
            // check if we have a trigger
            if (_phase == Phases.Analysing
                && _averageFilter == AverageFilter.Buy
                && _high != 0
                && _isEnabled
                && price.TimeStamp.TimeOfDay < TerrapinManagement.TIME_LAST_ENTRY)
            {
                if (_high == _low)
                {
                    if (Log.IsInfoEnabled)
                    {
                        Log.Info("{0}|{1}|CheckBuyEntry Filtered short entry at {2} due to channel range being 0 High({3}) Low({4})", _security, _id, price.Price, _high, _low);
                    }
                    return;
                }

                if (price.Price > _high)
                {
                    double exitPrice;
                    if (_bars5min.TryGetPreviousLow(_barIndex - 1, _numExitBars, out exitPrice))
                    {
                        //if (exitPrice < _bars5min.Low)
                        //{    
                        if (_isEnabled)
                        {
                            Phase = Phases.Triggered;
                            uint quantity = (uint)(_unitValue / (price.Price / _divisor));
                            Log.Info("{0}|{1}|CheckBuyEntry Entering long {2}@{3} BarHigh({4})", _security, _id, quantity, price.Price, _high);
                            _triggeredUnit = new TerrapinUnit(this, _tradingFeed, this, true, price.Price, exitPrice, quantity);
                            _manager.AddStrategyUnit(_triggeredUnit);
                            _triggeredUnit.EnterUnit(price.TimeStamp);
                        }
                        else
                        {
                            Log.Debug("{0}|{1}|CheckBuyEntry Disabled so filtered long entry at {2}", _security, _id, price.Price);
                        }
                        //}
                        //else if (Log.IsInfoEnabled)
                        //{
                        //    Log.Info("{0}|{1}|OnPriceAsk Filtered long entry at {2} due to range check Low({3}) ExitPrice({4})", _security, _id, ask.Price, _bars5min.Low, exitPrice);
                        //}
                    }
                }
            }
            else if (_phase == Phases.Triggered)
            {
                if (price.TimeStamp.TimeOfDay >= TerrapinManagement.TIME_EXIT_ALL)
                {
                    if (!_isExiting)
                    {
                        _isExiting = true;

                        Log.Info("{0}|{1}|CheckBuyEntry Exiting triggered unit due to time > than exit time {2}", _security, _id, TerrapinManagement.TIME_EXIT_ALL);
                        _triggeredUnit.ExitUnit(price.TimeStamp);
                    }
                }
                else if (_averageFilter == AverageFilter.Sell
                && _triggeredUnit != null)
                {
                    _triggeredUnit.OnBestPrice(price.Price, price.TimeStamp);
                }
            }
        }

        private void CheckSellEntry(Objects.Finance.Prices.PriceVolume price)
        {
            // check if we have a trigger
            if (_phase == Phases.Analysing
                && _averageFilter == AverageFilter.Sell
                && _low != 0
                && _isEnabled
                && price.TimeStamp.TimeOfDay < TerrapinManagement.TIME_LAST_ENTRY)
            {
                if (price.Price < _low)
                {
                    if (_high == _low)
                    {
                        if (Log.IsInfoEnabled)
                        {
                            Log.Info("{0}|{1}|CheckSellEntry Filtered short entry at {2} due to channel range being 0 High({3}) Low({4})", _security, _id, price.Price, _high, _low);
                        }
                        return;
                    }

                    double exitPrice;
                    if (_bars5min.TryGetPreviousHigh(_barIndex - 1, _numExitBars, out exitPrice))
                    {
                        //if (exitPrice > _bars5min.High)
                        //{
                        if (_isEnabled)
                        {
                            Phase = Phases.Triggered;
                            uint quantity = (uint)(_unitValue / (price.Price / _divisor));
                            Log.Info("{0}|{1}|CheckSellEntry Entering short {2}@{3} BarHigh({4})", _security, _id, quantity, price.Price, _low);
                            _triggeredUnit = new TerrapinUnit(this, _tradingFeed, this, false, price.Price, exitPrice, quantity);
                            _manager.AddStrategyUnit(_triggeredUnit);
                            _triggeredUnit.EnterUnit(price.TimeStamp);
                        }
                        else
                        {
                            Log.Debug("{0}|{1}|CheckSellEntry Disabled so filtered short entry at {2}", _security, _id, price.Price);
                        }
                        //}
                        //else if (Log.IsInfoEnabled)
                        //{
                        //    Log.Info("{0}|{1}|OnPriceBid Filtered short entry at {2} due to range check High({3}) ExitPrice({4})", _security, _id, bid.Price, _bars5min.High, exitPrice);
                        //}
                    }
                }
            }
            else if (_phase == Phases.Triggered)
            {
                if (price.TimeStamp.TimeOfDay >= TerrapinManagement.TIME_EXIT_ALL)
                {
                    if (!_isExiting)
                    {
                        _isExiting = true;

                        Log.Info("{0}|{1}|CheckSellEntry Exiting triggered unit due to time > than exit time {2}", _security, _id, TerrapinManagement.TIME_EXIT_ALL);
                        _triggeredUnit.ExitUnit(price.TimeStamp);
                    }
                }
                else if (_averageFilter == AverageFilter.Buy
                && _triggeredUnit != null)
                {
                    _triggeredUnit.OnBestPrice(price.Price, price.TimeStamp);
                }
            }

        }

        public void ExitUnit(DateTime timeStamp)
        {
            if (Monitor.TryEnter(_lock, 1000))
            {
                try
                {
                    if (_phase == Phases.Triggered && _triggeredUnit != null)
                    {
                        if (!_isExiting)
                        {
                            _isExiting = true;

                            if (Log.IsInfoEnabled)
                                Log.Info("{0}|{1}|ExitUnit exiting currently triggered unit at market", _security, _id);

                            _triggeredUnit.ExitUnit(timeStamp);
                        }
                    }
                    else if (Log.IsTraceEnabled)
                    {
                        Log.Trace("{0}|{1}|ExitUnit can not exit when not triggered", _security, _id);
                    }
                }
                finally
                {
                    Monitor.Exit(_lock);
                }
            }
            else if (Log.IsDebugEnabled)
            {
                Log.Debug("{0}|{1}|ExitUnit couldn't enter lock after 1 second", _security, _id);
            }
        }

        private void GetChannel(int barIndex)
        {
            double high, low;
            if (_bars5min.TryGetPreviousHighLow(barIndex - 1, _numEntryBars, out high, out low))
            {
                if (high != _high || low != _low)
                {
                    if (Log.IsDebugEnabled)
                        Log.Info("{0}|{1}|GetChannel has updated the channels from High:{2:F2} Low:{3:F2} to High:{4:F2} Low:{5:F2}", _security, _id, _high, _low, high, low);

                    _high = high;
                    _low = low;
                    NotifyPropertyChanged("High");
                    NotifyPropertyChanged("Low");
                }
            }
        }

        /*
        private void GetFilter(int barIndex)
        {
            _fastEMA = _bars1min.GetMovingAverage(barIndex, _periodFastEMA * BAR_SIZE, OHLCMinuteBars.Data.Close, OHLCMinuteBars.MAType.EMA);
            _slowSMA = _bars1min.GetMovingAverage(barIndex, _periodSlowSMA * BAR_SIZE, OHLCMinuteBars.Data.Close, OHLCMinuteBars.MAType.SMA);
            NotifyPropertyChanged("MAFast");
            NotifyPropertyChanged("MASlow");

            if (_fastEMA > _slowSMA)
            {
                if (_averageFilter != AverageFilter.Buy)
                {
                    if (Log.IsDebugEnabled)
                    {
                        Log.Debug("{0}|{1}|GetFilter has updated from {2} to Buy Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _fastEMA, _slowSMA);
                    }
                    Filter = AverageFilter.Buy;
                }
            }
            else if (_fastEMA < _slowSMA)
            {
                if (_averageFilter != AverageFilter.Sell)
                {
                    if (Log.IsDebugEnabled)
                    {
                        Log.Debug("{0}|{1}|GetFilter has updated from {2} to Sell Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _fastEMA, _slowSMA);
                    }
                    Filter = AverageFilter.Sell;
                }
            }
            else if (_averageFilter != AverageFilter.Flat)
            {
                if (Log.IsDebugEnabled)
                {
                    Log.Debug("{0}|{1}|GetFilter has updated from {2} to Flat Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _fastEMA, _slowSMA);
                }
                Filter = AverageFilter.Flat;
            }
        }
        */

        
        private void GetFilter(int barIndex)
        {
            _fastEMA = _bars1min.GetMovingAverage(barIndex, _periodFastEMA * BAR_SIZE, OHLCMinuteBars.Data.Close, OHLCMinuteBars.MAType.SMA);
            if (_slowSMA == 0)
            {
                _slowSMA = _fastEMA;
            }
   
            NotifyPropertyChanged("MAFast");
            NotifyPropertyChanged("MASlow");

            if (_fastEMA > _slowSMA)
            {
                if (_averageFilter != AverageFilter.Buy)
                {
                    if (Log.IsDebugEnabled)
                    {
                        Log.Debug("{0}|{1}|GetFilter has updated from {2} to Buy Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _fastEMA, _slowSMA);
                    }
                    Filter = AverageFilter.Buy;
                }
            }
            else if (_fastEMA < _slowSMA)
            {
                if (_averageFilter != AverageFilter.Sell)
                {
                    if (Log.IsDebugEnabled)
                    {
                        Log.Debug("{0}|{1}|GetFilter has updated from {2} to Sell Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _fastEMA, _slowSMA);
                    }
                    Filter = AverageFilter.Sell;
                }
            }
            else if (_averageFilter != AverageFilter.Flat)
            {
                if (Log.IsDebugEnabled)
                {
                    Log.Debug("{0}|{1}|GetFilter has updated from {2} to Flat Fast({3:F2}) Slow({4:F2})", _security, _id, _averageFilter, _fastEMA, _slowSMA);
                }
                Filter = AverageFilter.Flat;
            }
        }
        

        public void OnTerrapinUnitComplete(TerrapinUnit unit)
        {
            lock (_lock)
            {
                if (Log.IsInfoEnabled)
                    Log.Info("{0}|{1}|OnTerrapinUnitComplete has recieved a completed unit with a profit of {2}", _security, _id, unit.Profit);

                // get the channel that includes the current bar
                //GetChannel(_barIndex + 1);
                //_barsInitCount -= 1;

                Phase = Phases.BuildingData;
                _triggeredUnit = null;
                _isExiting = false;
            }
        }

        public void OnTerrapinUnitFailed(TerrapinUnit unit)
        {
            lock (_lock)
            {
                if (Log.IsInfoEnabled)
                    Log.Info("{0}|{1}|OnTerrapinUnitFailed has failed to fill a unit so is disabling this unit", _security, _id, unit.Profit);

                IsEnabled = false;

                // get the channel that includes the current bar
                GetChannel(_barIndex + 1);
                _barsInitCount -= 1;

                Phase = Phases.FillFailure;
                _triggeredUnit = null;
                _isExiting = false;
            }
        }

        #endregion
        //////////
        #region Enums

        public enum Phases
        {
            Initialised,
            BuildingData,
            Analysing,
            Triggered,
            FillFailure
        }

        public enum AverageFilter
        {
            Buy,
            Sell,
            Flat
        }

        #endregion

    }
}